/**
 * This file is part of the Kompics P2P Framework.
 *
 * Copyright (C) 2009 Swedish Institute of Computer Science (SICS)
 * Copyright (C) 2009 Royal Institute of Technology (KTH)
 *
 * Kompics is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License
 * as published by the Free Software Foundation; either version 2
 * of the License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
 */
package se.sics.drsm.bootstrap.client;

import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.UUID;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import se.sics.drsm.bootstrap.events.DRSMBootstrapCompleted;
import se.sics.drsm.bootstrap.events.DRSMBootstrapResponse;
import se.sics.drsm.bootstrap.port.DRSMBootstrapPort;
import se.sics.drsm.bootstrap.server.events.DRSMCacheAddPeerRequest;
import se.sics.drsm.bootstrap.server.events.DRSMCacheGetPeersRequest;
import se.sics.drsm.bootstrap.server.events.DRSMCacheGetPeersResponse;
import se.sics.drsm.overlay_address.key.NumericRingKey;
import se.sics.drsm.port.drsm_bootstrap.DRSMPeerEntry;
import se.sics.kompics.ComponentDefinition;
import se.sics.kompics.Handler;
import se.sics.kompics.Negative;
import se.sics.kompics.Positive;
import se.sics.kompics.address.Address;
import se.sics.kompics.network.Network;
import se.sics.kompics.p2p.bootstrap.BootstrapCacheReset;
import se.sics.kompics.p2p.bootstrap.BootstrapRequest;
import se.sics.kompics.p2p.bootstrap.client.BootstrapClientInit;
import se.sics.kompics.p2p.bootstrap.client.ClientRefreshPeer;
import se.sics.kompics.p2p.bootstrap.client.ClientRetryRequest;
import se.sics.kompics.p2p.bootstrap.server.CacheResetRequest;
import se.sics.kompics.timer.CancelTimeout;
import se.sics.kompics.timer.ScheduleTimeout;
import se.sics.kompics.timer.Timer;

/**
 * The <code>BootstrapClient</code> class.
 *
 * @author Cosmin Arad <cosmin@sics.se>
 * @author Shadid Chowdhury <msrch@kth.se>
 */
public class DRSMBootstrapClient extends ComponentDefinition {

    Negative<DRSMBootstrapPort> bootstrap = negative(DRSMBootstrapPort.class);

    Positive<Network> network = positive(Network.class);
    Positive<Timer> timer = positive(Timer.class);

    private static Logger logger = LoggerFactory
            .getLogger(DRSMBootstrapClient.class);

    private final HashSet<UUID> outstandingTimeouts;
    private BootstrapRequest activeBootstrapRequest;

    // private HashSet<DRSMPeerEntry> overlays;
    private Map<NumericRingKey, Set<DRSMPeerEntry>> overlays;

    private Address bootstrapServerAddress;
    private Address self;
    private int clientWebPort;
    private long refreshPeriod;
    private long retryPeriod;
    private int retriesCount;

    public DRSMBootstrapClient() {
        outstandingTimeouts = new HashSet<UUID>();
        // TODO: or to use synchronized map for avoiding concurrency?
        overlays = Collections
                .synchronizedMap(new HashMap<NumericRingKey, Set<DRSMPeerEntry>>());

        subscribe(handleInit, control);

        subscribe(handleBootstrapRequest, bootstrap);
        subscribe(handleBootstrapCompleted, bootstrap);
        subscribe(handleBootstrapCacheReset, bootstrap);
        subscribe(handleCacheGetPeersResponse, network);
        subscribe(handleClientRefreshPeer, timer);
        subscribe(handleClientRetryRequest, timer);
    }

    Handler<BootstrapClientInit> handleInit = new Handler<BootstrapClientInit>() {
        public void handle(BootstrapClientInit init) {
            refreshPeriod = init.getBootstrapConfiguration()
                    .getClientKeepAlivePeriod();
            retryPeriod = init.getBootstrapConfiguration()
                    .getClientRetryPeriod();
            retriesCount = init.getBootstrapConfiguration()
                    .getClientRetryCount();
            bootstrapServerAddress = init.getBootstrapConfiguration()
                    .getBootstrapServerAddress();
            clientWebPort = init.getBootstrapConfiguration().getClientWebPort();
            self = init.getSelf();

            logger.debug("bootstrapServerAddress {}", bootstrapServerAddress);
        }
    };

    private Handler<BootstrapRequest> handleBootstrapRequest = new Handler<BootstrapRequest>() {
        public void handle(BootstrapRequest event) {
            // set an alarm to retry the request if no response
            ScheduleTimeout st = new ScheduleTimeout(retryPeriod);
            ClientRetryRequest retryRequest = new ClientRetryRequest(st,
                    retriesCount, event);
            st.setTimeoutEvent(retryRequest);
            UUID timerId = retryRequest.getTimeoutId();
            DRSMCacheGetPeersRequest request = new DRSMCacheGetPeersRequest(
                    event.getOverlay(), event.getPeersMax(), timerId, self,
                    bootstrapServerAddress);

            outstandingTimeouts.add(timerId);

            activeBootstrapRequest = event;

            trigger(request, network);
            trigger(st, timer);

            logger.debug("@{}: Sending GetPeersRequest to ", self.getId(),
                    bootstrapServerAddress);
        }
    };

    private Handler<ClientRetryRequest> handleClientRetryRequest = new Handler<ClientRetryRequest>() {
        public void handle(ClientRetryRequest event) {
            if (!outstandingTimeouts.contains(event.getTimeoutId())) {
                return;
            }
            outstandingTimeouts.remove(event.getTimeoutId());

            if (event.getRetriesLeft() > 0) {
                // set an alarm to retry the request if no response
                ScheduleTimeout st = new ScheduleTimeout(retryPeriod);
                ClientRetryRequest retryRequest = new ClientRetryRequest(st,
                        event.getRetriesLeft() - 1, event.getRequest());
                st.setTimeoutEvent(retryRequest);
                UUID timerId = retryRequest.getTimeoutId();
                DRSMCacheGetPeersRequest request = new DRSMCacheGetPeersRequest(
                        event.getRequest().getOverlay(), event.getRequest()
                        .getPeersMax(), timerId, self,
                        bootstrapServerAddress);

                outstandingTimeouts.add(timerId);

                activeBootstrapRequest = event.getRequest();

                trigger(request, network);
                trigger(st, timer);

                logger.debug("@{}: Sending GetPeersRequest to  ", self.getId(),
                        bootstrapServerAddress);
            } else {
                DRSMBootstrapResponse response = new DRSMBootstrapResponse(activeBootstrapRequest,
                        false, activeBootstrapRequest.getOverlay(), null, null);
                trigger(response, bootstrap);
            }
        }
    };

    private Handler<DRSMCacheGetPeersResponse> handleCacheGetPeersResponse = new Handler<DRSMCacheGetPeersResponse>() {
        public void handle(DRSMCacheGetPeersResponse event) {

            logger.debug(
                    "@{}: Received GetPeersResponse {}. Triggering bootstrap response.",
                    self.getId(), event.getPeers());

            if (outstandingTimeouts.contains(event.getRequestId())) {
                CancelTimeout ct = new CancelTimeout(event.getRequestId());
                trigger(ct, timer);
                outstandingTimeouts.remove(event.getRequestId());
            } else {
                // request was retried. we ignore this first slow response.
                // (to avoid double response;TODO add a local BOOTSTRAPPED flag
                // per overlay)
                return;
            }

            // TODO request map for MULTIPLE overlays
            DRSMBootstrapResponse response = new DRSMBootstrapResponse(
                    activeBootstrapRequest, true,
                    activeBootstrapRequest.getOverlay(), event.getPeers(),
                    event.getNumericRingKey());

            logger.debug(
                    "@{}: Received GetPeersResponse {}. Triggering bootstrap response.",
                    self.getId(), event.getPeers());
            trigger(response, bootstrap);
        }
    };

    //TODO: Think about changing the protocol to make it efficient. Do we really need to send the whole peer list
    // on add request.
    private Handler<DRSMBootstrapCompleted> handleBootstrapCompleted = new Handler<DRSMBootstrapCompleted>() {
        public void handle(DRSMBootstrapCompleted event) {
            logger.debug("Bootstrap completed.");
            NumericRingKey ringKey = event.getOverlayAddress().getKey();
            DRSMPeerEntry peerEntry = new DRSMPeerEntry(event.getOverlay(),
                    event.getOverlayAddress(), clientWebPort, 0, 0);

            Set<DRSMPeerEntry> peerList = (Set<DRSMPeerEntry>) overlays
                    .get(ringKey);

            if (peerList == null) {
                peerList = Collections
                        .synchronizedSet(new HashSet<DRSMPeerEntry>()); // TODO
                // use
                // concurrent
                // set
                // instead?
            }

            peerList.add(peerEntry);
            overlays.put(ringKey, peerList);

            logger.debug("Bootstrap completed. Added peerEntry {}", peerEntry);

            DRSMCacheAddPeerRequest request = new DRSMCacheAddPeerRequest(self,
                    bootstrapServerAddress, ringKey, peerEntry,
                    (Map<NumericRingKey, Set<DRSMPeerEntry>>) overlays);

            trigger(request, network);

            // set refresh periodic timer
            // TODO: Why is it like this?
            ScheduleTimeout st = new ScheduleTimeout(refreshPeriod);
            st.setTimeoutEvent(new ClientRefreshPeer(st));
            st.setTimeoutEvent(new ClientRefreshPeer(st));
            st.setTimeoutEvent(new ClientRefreshPeer(st));
            st.setTimeoutEvent(new ClientRefreshPeer(st));
            st.setTimeoutEvent(new ClientRefreshPeer(st));
            st.setTimeoutEvent(new ClientRefreshPeer(st));
            st.setTimeoutEvent(new ClientRefreshPeer(st));
            st.setTimeoutEvent(new ClientRefreshPeer(st));
            st.setTimeoutEvent(new ClientRefreshPeer(st));
            st.setTimeoutEvent(new ClientRefreshPeer(st));
            st.setTimeoutEvent(new ClientRefreshPeer(st));
            st.setTimeoutEvent(new ClientRefreshPeer(st));
            st.setTimeoutEvent(new ClientRefreshPeer(st));
            st.setTimeoutEvent(new ClientRefreshPeer(st));
            st.setTimeoutEvent(new ClientRefreshPeer(st));
            st.setTimeoutEvent(new ClientRefreshPeer(st));
            st.setTimeoutEvent(new ClientRefreshPeer(st));
            trigger(st, timer);
        }
    };

    private Handler<ClientRefreshPeer> handleClientRefreshPeer = new Handler<ClientRefreshPeer>() {
        public void handle(ClientRefreshPeer event) {
            //FIXME
//			DRSMCacheAddPeerRequest request = new DRSMCacheAddPeerRequest(self,
//					bootstrapServerAddress, null, null, //TODO: update this protocol to avoid sending null
//					(Map<NumericRingKey, Set<DRSMPeerEntry>>) overlays);
//			trigger(request, network);
        }
    };

    private Handler<BootstrapCacheReset> handleBootstrapCacheReset = new Handler<BootstrapCacheReset>() {
        public void handle(BootstrapCacheReset event) {
            CacheResetRequest request = new CacheResetRequest(self,
                    bootstrapServerAddress, event.getOverlay());
            trigger(request, network);
        }
    };
}
